设计模式
迭代器模式
提供一个方法顺序的访问一个聚合对象中的各个元素,而不暴露其内部的表示。
Aggregate接口:需要遍历的对象的“集合”(抽象)
1
2
3public interface Aggregate{
public abstract iterator iterator();
}Iterator:遍历的方式
1
2
3
4public interface Iterator {
public abstract boolean hasNext();
public abstract Object next();
}
迭代器模式的意义?
把存取数据的结构和遍历数据的方式分离开,使得使用者无需关注我是使用什么存储结构而可以遍历我的对象。
通俗点说,考虑这样两个对象:1
2
3
4
5
6
7
8
9class Food{}
public class AMenu{
ArrayList<Food> menu;
}
public class BMenu{
Food[] menu;
}
传统的遍历方式,现在有一个服务员,需要打印两个菜单:1
2
3
4
5
6
7
8public class Waitress{
AMenu A = new AMenu();
BMenu B = new BMenu();
void printMenu(){
//遍历两个菜单中的数据
}
}
有两个问题:
- Waitress可以直接访问菜单对象中的数据,不满足封装的定义。
- 对于两个菜单就需要写两种遍历方式,增加菜单就需要再增加遍历的方法,不符合开闭原则,这是一段易变的代码,应该抽取出来。
- 上述创建对象的方式不对,不符合依赖转置原则,应该面对接口编程,同时可以使用工厂模式来处理易变代码。
对于遍历的部分,就可以使用迭代器模式进行优化。
优化后:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class AMenu{
ArrayList<Food> menu;
Iterator iterator(){}
}
public class BMenu{
Food[] menu;
Iterator iterator(){}
}
public class Waitress{
AMenu A = new AMenu();
BMenu B = new BMenu();
void printMenu(){
print(A.iterator());
print(B.iterator());
}
void print(Iterator it){
//遍历
}
}
上面代码还可以优化,每个菜单都要打印一次,不符合开闭原则,不如把所用菜单放到List中,再使用一次迭代器。
如何菜单中又有菜单,如何遍历每一个菜品?
想象一下这个菜单类,他既拥有菜品类,又有本类。如何设计?如何遍历?
难道说,需要每次都使用If判断一下所调用的对象类型么?这样处理无论是在新增菜品,还是在遍历菜单的时候都会非常的麻烦。
建造者模式
定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
如何理解“构建”和“表示”
构建表示:创建一个对象组件的过程
表示:将组件按各种规则组合后的最终产品
不使用建造者模式会有什么问题?
1 |
|
访问者模式
基本概念
表示一个作用于某对象结构中的各元素的操作,可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
- 解耦数据结构和操作(算法),使得操作易扩展
- 适用于数据结构比较固定,操作异变
概念分析
实现男人和女人在不同行为下的不同社会舆论:
1 | abstract class BasePerson{ |
问题:每增加一种新的行为(如结婚),男人的女人的数据结构中的行为代码就需要进行一定的修改,不符合开闭原则
第一次改进:使用行为类来实现
1 | abstract class Action { |
好处:如果要增加对“结婚”对比,只需要增加“结婚”的行为类,不需要改动以往的任何类的代码。
访问者模式的局限(或者说使用范围)?
如上面例子中,如果人的性别不只是男女,而是更多,那么每一个Action中都需要增加对应的操作,显然不符合开闭原则。因此,访问者的使用方位是访问的对象(这里称为数据结构)需要是比较稳定。
为什么需要使用访问者模式?
访问者模式的目的是将处理方法从数据中分离出来,分为数据结构
和访问者(操作)
,很多系统都可以按照算法和数据结构分开,数据结构是比较稳定的,而算法易于变化,这时使用访问者模式就非常合适。
工厂模式
工厂模式解决什么问题?
解决“new”的具体对象生成方式。
工厂的意义就是把创建的过程封装起来,把对象的生成和使用分离开来。
客户不关心生产过程,只在乎结果。
生活中也常常将生产和使用分开,因为彼此有不同的收益。
new有什么问题?
new实际上是一种硬编码,是代码中易变的一部分,不符合两个设计原则:
- 把易变的部分独立出来。
- 代码应该对修改关闭,对扩展打开。
这两个原则都意味着 new 是一种不太好的方式
不适用工厂会有什么问题?
接下里分析一些案例代码来进一步体会:
问题代码:
1 | public class OrderPizza { |
上面代码的问题是,没有对修改关闭。
如何第一次优化?
把易变的部分提取出来封装:
简单工厂
1 | class PizzaStore { |
上面的方法叫做简单工厂,并不属于一种设计模式,但是是比较常用的手法。
额外的一个好处是,可以让这段代码在其他地方复用。
简单工厂的定义只有一个工厂类,不好拓展。
可以使用static修辞符,叫做静态工厂,好处是不用创建对象,坏处是无法使用继承来改变创建方式。
如何第二次优化?
工厂方法
成为设计模式的是工厂方法,和简单工厂本质上是一样的,如下:
1 | abstract class PizzaStore { |
可以看出,最主要是由 组合 变为了 继承。可以通过继承子类进行拓展。
上面的工厂都只是创建了一个产品对象,而生活中的工厂可以生产一簇产品(比如各种汽车工厂)。
如何第三次优化?
其实就是从简单工厂方法中提炼出抽象工厂,再抽象接口中定义多个产品。
工厂方法模式和泛型
1 | //Product |
抽象工厂
1 | public interface PizzaIngredientFactory{ |
抽象方法在产品的维度上更进一步。
观察者模式
也叫“发布订阅模式”
对象之间一对多的依赖,当一个对象的状态发生变化的时候,依赖它的对象都会收到通知,并且自动更新。
被观察者不需要知道具体的观察者都有哪些,因为保存的是接口列表,面向接口编程,也就是松耦合。
至于为什么使用接口可以解耦,请看我之前关于“接口”的部分
设计原则:减少紧耦合的代码
生活案例:
我订阅了一家报社,报社每次更新就会通知我,之后有两种方案:
- 推:直接把所有新报纸发给我
- 拉:由我去选取感兴趣的报纸。
如果不想收到通知了就退订。
程序案例:
JAVA内置的观察者模式
监听器:使用事件来处理状态的变化(事实上等价于由事件来触发state的改变)
监听器(观察者)
主题分解为事件、事件源、事件发布器:
事件(状态变化的抽象),事件源(事件发生的对象),事件发布器(负责注册监听器、触发事件、消息通知)
观察者模式典型实现方式:
- 定义2个接口:观察者(通知)接口、被观察者(主题)接口
- 定义2个类,观察者对象实现观察者接口、主题类实现被观者接口
- 主题类注册自己需要通知的观察者
- 主题类某个业务逻辑发生时通知观察者对象,每个观察者执行自己的业务逻辑。
命令模式
命令模式解决什么问题?
命令模式是一个行为模式
所谓行为,就是某个对象实现了某个功能的一个过程,比如灯开了,音乐响了,厨师开始炒肉了….
从对象语言的角度看,就是一个对象执行了一个方法。
命令模式解决的两个问题:
将行为的定义和行为的执行解耦
解耦的意义?
命令执行者不需要关心行为的定义是什么,只需要在某个条件触发时去执行;
行为的定义只需要满足一定的格式就可以得到拓展。符合开闭的原则。将行为抽象成命令,进行统一的管理控制
抽象成命令的意义?
可以采用各种数据结构各种算法来操作这些命令,实现更多的功能,譬如撤销,队列,池等等。
工作模式
最核心的肯定是命令的抽象:1
2
3public interface Command {
public void execute();
}
现在可以分两个层面来分析命令模式的工作:
- 命令的具体定义
- 管理控制命令
这两个问题是互相独立的,互不干扰。
命令的具体定义
命令的具体定义?
1 | public class ConcreteCommand1 { |
命令一般并不实现具体的行为,具体行为是由真正的执行者去实现的。
当然命令本身也可以成为真正的执行者。
执行者:1
2
3public class Receiver1 {
public void action();
}
执行者一般来源于第三方
如何将执行者和命令绑定起来?
因此需要在命令对象中保存真正的执行者对象,命令作为一个委托者来调用执行者的方法。
将执行者和命令绑定起来的地方成为Client
Client:1
2
3public class Client {
Command command1 = new ConcreteCommand1(Receiver1);
}
执行者和命令的关系:
有上面封装的过程可以看到,执行者和命令之间是什么紧密的,基本上可以认为一个行为就需要一个新的命令去封装。
但是,这些过程对于调用方来说都是看不见的,调用者也不需要去关心命令的具体实现。
因而一旦命令被封装好,就按照命令的属性来进行操作。
管理控制命令
调用方操作的命令都是抽象的,符合依赖转置原则。
对于调用方而言,所有的命令都是一些具有方法的普通对象。
调用方将所有的命令采用合适的数据结构保存起来,然后根据功能去操作这些命令。
1 | public class Invoker { |
常见的管理命令的操作:撤销,队列,宏
常见的命令模式案例:线程池
代理模式
组合模式
将对象组合成树形结构以表示‘部分-整体’的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
需求中是体现部分与整体层次的结构时,以及希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就应该考虑组合模式了。
策略模式
策略模式是一种定义一系列算法的方法,从概念上来看,所有这些算法完成的都是相同的工作,只是实现不同,它可以以相同的方式调用所有的算法,减少了各种算法类与使用算法类之间的耦合。
当不同的行为堆砌在一个类中时,就很难避免使用条件语句来选择合适的行为。将这些行为封装在一个个独立的Strategy类中,可以在使用这些行为的类中消除条件语句。
只要在分析过程中听到需要在不同时间应用不同的业务规则,就可以考虑使用策略模式处理这种变化的可能性。
模板方法模式
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
原型模式
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
实现clone()方式时需要考虑深复制和浅复制的问题
适配器模式
将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
系统的数据和行为都正确,但接口不符时,我们应该考虑用适配器,目的是使控制范围之外的一个原有对象与某个接口匹配。适配器模式主要应用于希望复用一些现存的类,但是接口又与复用环境要求不一致的情况。
装饰者模式
开闭原则:不允许修改,只允许扩展。
符合开闭原则的好处?
可容易应对易变的代码,接受新的功能,而不破坏以前的功能。
装饰者模式完全符合开闭原则
并不是代码中的每个部分都需要满足开闭原则,那样只会增加代码的复杂度,没有任何好处。需要处理的地方是代码中比较重要的易变部分,针对这一部分运用开闭原则更有益于维护。
使用场景
- 为了方便统一管理,把多个不同的类,适配成同一个类进行管理,如GenericConverter
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
注意单例中的多线程安全问题
解决方案:
- 双重锁
- 静态初始化